fix(app): preserve readable conversation history and recover without replay - #5328
Conversation
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed full PR at 2a0f224259be1a160991d01adb4eccd4f0488a01, against base 649826221289cd4cb3dd8880d016e0afbbaca0fc. 没有未解决的阻塞项。结论覆盖历史恢复这一可独立验收的增量;不把读取恢复视为原生委派、接收方采用或完整 GQ09 已通过。
动机
普通任务是“接着昨天的做”。旧版对所有 Session 使用 Promise.all:一个旧记录返回 503,当前可读消息也不显示,界面还可能呈现空会话。不可用的读取应当明确显示,不能成为重放任务或另建驱动的理由。此次改动使已有对话、结果位置和草稿在这种失败中可继续使用,直接改善 R1 恢复路径。
改动思路
最小的 Promise.allSettled 替换只能保住初次读取,仍缺少缺失记录恢复、实时消息去重和当前执行器的发送判定。采用现有 Chat HTTP/file store、reconcileConversationReturns 和会话恢复入口;新 useConversationHistory 只是管家与 Goal 共用的 TS 读取 owner。普通历史重读和有回传义务的轮询保持不同触发条件,但共享同一消息身份和合并规则。没有新增 capability、权限、收件箱、lease、模型调用或并行 Python 决策源。
具体改动
关键代码讲解
chat.ts:789的fetchChatHistory保留可读快照,单次读取有超时,退避重读仅覆盖缺失项;消息身份从单独 message ID 改为 Session+message ID。现有真实 HTTP 列表按 channel/Goal/Agent 过滤,无展示截断被误当完整来源的问题。use-conversation-history.ts:14的useConversationHistory在当前选择的执行器上判定记录是否可读;更“新”的其他执行器 Session 不能绕过等待。旧记录恢复只补历史,不重新连接当前流。作用域切换取消旧写回,成功完整读取不持续轮询。conversation-returns.ts:64的reconcileConversationHistory/reconcileConversationReturns保留实时文本、缺失时的最后回传状态和原始位置;同一 Turn 的用户消息与答复分别绑定。Dashboard 给已接受消息和状态投影写入准确身份,导航回来不产生重复消息。personal-workspace-page.tsx:1574的PersonalWorkspacePage.sendMessage与按钮共用发送判定,Enter 也不能绕过;草稿保持,恢复后在原 Session 提交一次。中英提示、单个重试按钮和现有主题样式都放在输入区旁,桌面与窄屏已实际渲染。- Python
chat_server.py:614的_record_projection_exchange返回现有写入产生的两个 message ID,仍是原 v1 的附加字段,未增加副作用;真实服务器已逐项对照落盘消息。registry-I/O census 只修正该文件引起的两个行号变化,分类未改。 - 单元、真实 HTTP/file-store 和已有 packaged 浏览器框架补上负例;现有 fixture 的省略执行器行为按实际 host 默认值纠正,全套 25 场景通过。RFC 将共享读取恢复及其验收边界写清。
对主干的风险
默认变化是:当前已列出的 Session 无法读取时,普通发送等待原记录恢复;旧记录不可读而当前可读时仍能发送。这是明确的正确性条件,已在 RFC、PR 和负例披露。重试会产生 GET,按 3–30 秒退避,没有任务重放;持续存储故障仍会保持等待,需要原读取来源恢复。
反向风险也验证了:只坏旧记录不应过度阻塞;只坏当前记录不能被别的执行器或 Enter 绕过;恢复不丢草稿、不重复请求;实时答复和状态投影均能跨导航保留正确身份。真正的存储故障通过隔离 Chat HTTP/file backend 验证;浏览器的执行答复为脚本化 fixture,因此不证明 live provider 或接收方实际采用。
验证包括 strict TS/打包、回传单元测试、真实 Chat server、105 项现有 Chat 回归、25 个 packaged 浏览器场景,以及相同 harness 的 base/head 反证:base 在真实 GET 和可读当前消息的浏览器断言上均失败,head 通过。最终 head 的 16 项 selected+5 项 direct premerge 检查全部通过,无失败、超时或跳过;首次 premerge 发现的 census 行号过期已用现有生成器修正。功能验证在 16d055fa6 完成;之后唯一源码差异是这两个 census 行号,最终 head 再跑原生检查和 exact-scope quality verify。未查询或等待远端 CI。
语义与 CI 对齐
沿用现有 Chat Session、Turn 和回传位置词汇,读取状态是 TS 派生投影。新增 message ID 来自已有落盘记录,不构成新的授权或协作协议。原生检查采用当前仓库本地要求,UI 另外用 strict TS、真实 HTTP 和 packaged 交互补足;没有把远端 CI 当作评审依据。
我的整体评价
APPROVE,完整交付历史恢复这一范围。 相比只吞掉异常,这个增量让用户能看见已有工作、保留输入、恢复后继续一次,且通用 TS owner 可用于管家与 Goal。相关 #5293 的原始回传定位契约继续复用;本 PR 没有借机重写委派或协调。未来扩展仍围绕该读取边界,不需要复制一个 manager 专用实现。
最强的剩余验证是安装后的原生负责人接收与采用闭环,本 PR 未声称完成它;持续性规模与损坏的 Session 索引也不在当前 HTTP 读取故障范围内。界面证据使用 PR 中的合成 before/after、窄屏和当前记录等待截图。依照仓库规则,运行时/产品变更留给维护者合并。
English verdict: APPROVE - HEAD 2a0f224 preserves readable history and exact message lineage, retries reads without replay, and resumes the original Session once. Real HTTP/store, packaged negative paths, baseline counterfactuals and final-head premerge passed; native adoption and installed rollout remain unclaimed.
|
This pull request has merge conflicts with Choose the remote for the base repository, not an out-of-date fork. git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEADFor a same-repository clone whose Keep the DCO |
cocolord
left a comment
There was a problem hiding this comment.
动机
这个 PR 解决的是一个真实且高影响的 App 连续性问题:旧实现用 Promise.all 读取整个会话历史,只要一个旧 Session 的快照暂时失败,所有仍可读的消息都会一起消失。对用户而言,这不仅丢上下文,还可能让“接着昨天的做”落到错误的新会话。exact head 2a0f224259be1a160991d01adb4eccd4f0488a01 把失败收敛到单个 Session:旧记录失败时继续展示可用消息;当前执行器的最新 Session 失败时则保留草稿并阻止发送,直到原记录恢复。
改动思路
实现把读取、执行和展示的责任分开。fetchChatHistory 只做 GET 聚合,用 allSettled 留住成功快照,并在同一恢复 episode 中只补读 missing Session;useConversationHistory 以 agent/channel/Goal scope 管理 loading、partial、unavailable、3–30 秒退避、手动 retry 和当前 Session 可读性;原有 Dashboard effect 仍负责 resume_latest 与流式 Turn,不把读失败变成模型重放。消息合并继续复用既有 return reconciler,并用 session_id:message_id 以及 turn_id + role 保持身份。正向路径是旧 Session 503 -> 可读消息继续显示 -> 仅重试缺失 GET -> 恢复一条旧消息;负向路径是当前 Session 503 -> sendBlocked 和 connectionKey=null -> 按钮、Enter、快捷提问都不能发 -> 恢复后只向原 Session 提交一次。
具体改动
关键代码讲解
fetchChatHistory(apps/presentation/dashboard/src/data/chat.ts:789)将全有或全无的Promise.all改成 partial aggregate;跨 Session 的相同 message ID 以 Session 前缀隔离,成功快照缓存,失败 ID 单独重试,恢复过程没有 POST 或 Turn。useConversationHistory(apps/presentation/dashboard/src/data/use-conversation-history.ts:14)是管家与 Goal 共用的读取 owner。它按精确 scope 取消旧 timer/晚到结果,并只用所选 executor 的最新 Session 推导sendBlocked,不能拿另一个 executor 的可读记录绕过。reconcileConversationHistory/reconcileConversationReturns(conversation-returns.ts:29,64)保留 live text 与最后一次 receipt/collaboration 状态,用 role 区分同一 Turn 的 user/assistant,再按存储时间补入新恢复消息;状态投影写回后也用真实 message IDs 去重。PersonalWorkspacePage.sendMessage(personal-workspace-page.tsx:1574)成为按钮、Enter 和 quick prompt 的共同硬 gate;输入区增加中英文 loading/partial/unavailable/current-session 文案及 retry,CSS 只增加一组局部样式。ChatRequestHandler._record_projection_exchange(loopx/chat_server.py:614)仍写原来的两条状态投影消息,只在 v1 response 中附加实际落盘的 user/answer ID。loopx-chat-server-smoke.py对照这两个 ID;registry I/O manifest 只刷新受行号移动影响的 locator。- 新增的 unit、真实 HTTP/file-store smoke 和 packaged browser scenario 分别覆盖 Session-ID collision、same-Turn role、live-text preservation、单 Session 503、missing-only retry、store 不变、零 Turn、当前 Session gate、草稿保留、原 Session 单次恢复及 projection identity;RFC 同步说明读取恢复不代表 receiver adoption。
对主干的风险
功能本身在 reviewed head 上表现扎实:npm run build、reconciliation unit、真实 Chat HTTP/store smoke 和完整 25 个 packaged browser 场景都通过;真实 store 在故障恢复前后 byte-identical、Turn 数为 0。1512×982 与 390×844 截图已人工检查,没有横向溢出,提示与 retry 靠近 composer;但 Chrome skill 当前无法建立浏览器 binding,所以这里不声称完成 logged-in/live-provider Chrome 验证。远端三个 pytest 失败和 Frontstage v1.2.3 marker 已用相同命令在 immutable base 与 head 复现,测试 ID、断言和值一致,属于旧基线问题。
当前仍有一个 P1 blocker:目标分支已经从 PR 的 merge base 前进 31 个 commit,GitHub 现在明确返回 mergeable=false / DIRTY。只读 merge-tree 定位出四个 content conflict:personal-workspace-page.tsx、dashboard-page.tsx、conversation RFC 和 packaged scenario catalog。这里不是机械选边:main 新增了 followConversationRef、message updatedAt/preparing 与 conversation-startup scenario,本 PR 新增了 send gate、identity filter 与 history scenario。取任一侧都会静默丢掉另一侧的已验证语义。最小修复是基于最新 main 解决四处冲突,同时保留两套行为和两个 scenario,再运行完整 build、两个 focused history checks、完整 packaged suite 与 required checks;新 head 需要重新 review。
语义与 CI 对齐
读取状态是 TypeScript 派生 literal state,匹配使用精确 agent/Session/Turn/role/message ID,没有 substring denylist;Python 只返回既有持久化写入的 identity,不复制 UI 状态机。文案保持 domain-neutral,并明确区分 informational partial notice 与 machine-enforced sendBlocked。默认行为变化已在双语 RFC、UI 和 named smokes 中披露。
我的整体评价
结论是 REQUEST_CHANGES,原因仅是当前 exact head 无法与最新 main 干净集成,而且冲突落在同一条 composer/stream/message-lifecycle 主路径,不能把已通过的旧基线验证当成可合并证据。除此之外,收益已经被正负路径、真实 store 与完整 packaged UI 充分证明,551/86 的规模也主要来自端到端验证和协议说明,整体是合理的共享边界,没有新增第二权威。future-facing pass 的结论是保留现有 shared hook/reconciler,不再抽象;当前要做的是正确整合 main,而不是扩范围。
English verdict: REQUEST_CHANGES on exact head 2a0f224259be1a160991d01adb4eccd4f0488a01. The partial-history design and its zero-replay, exact-Session recovery behavior pass focused real-store, full build, and all 25 packaged browser scenarios. However, current main is 31 commits ahead and GitHub reports this head as conflicting; four content conflicts overlap composer, streaming identity/lifecycle, RFC, and browser-scenario ownership. Rebase, preserve both branches' semantics, rerun the listed checks, and request exact-head re-review.
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
…nuity-0930 Signed-off-by: huangruiteng <huangrt01@163.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Exact reviewed head: c27f7db9c46ee5d51d9ff5e21874f508effc2e86; integrated base: 2c52109777998b37e86c1fefa8eede07337af526.
动机
整个 PR 要修的是“历史读失败把对话一起带走”的体验:一个旧 Session 读取失败不应该隐藏当前可读消息、丢草稿、猜测替代会话或重新执行用户请求。已有工作应可恢复并继续,而不是要求用户重新说明。本次完整读取全量 diff,并重新验证主干新增 startup/活动反馈与本 PR 的恢复语义组合。
改动思路
复用既有 ChatSessionStore、GET history/snapshot、执行器连接和回传协议。新的 TypeScript useConversationHistory 是管家与 Goal 共用的读恢复 owner,不创建第二份持久状态或执行权限。列表/快照按 Session 归属,失败不是空历史;可读部分先展示,只重试缺失快照。当前选中执行器的最新 Session 读回成功后才允许发送,其他执行器的可读记录不能代替它授权续接。
具体改动
关键代码讲解
data/chat 使用 allSettled、读取 timeout 与 missing-only recovery;消息 identity 改为 Session+message。共用 hook 提供状态、手动重试和 3–30 秒退避,切换上下文时取消旧结果。reconcileConversationHistory/Returns 按 exact Session/message/turn+role 合并,保留实时文本及异步回传;DashboardPage 合并主干 onPhase 活动和生命周期,接收真正存储的 projection message IDs,去掉重复行。composer 保留草稿,当前记录不可读时点击和 Enter 同样受约束。Python server 只返回现有写入的标识,不增加写入 owner;I/O census 与 RFC 随调用更新。真实 HTTP/store、既有 returns/server 测试和浏览器目录共同覆盖恢复。
此前四处主干冲突已保留双方语义解决。相邻浏览器 oracle 修正已随新主干吸收,不再是当前 PR 的差异:主干已批准的模型设置按钮是 28px,普通只读标签仍受 26px 上限,窄屏仍是显式 44px;没有改产品尺寸。原测试在 main/head 同样失败,原因是旧标签断言遗漏新按钮契约,修正后整套 27 场景通过。
对主干的风险
语义与 CI 对齐
主要风险是跨上下文迟到结果覆盖、重连产生重复效果,以及恢复行覆盖正在输出的正文。精确提交打包 build、27 项真实浏览器场景、returns 回归、真实 HTTP/file-store smoke、server smoke 和 71 项 native manager 回归通过。相同不可变主干页面复现一条旧历史失败后当前消息消失,本 head 保留消息并恢复;缺失读取重试零写入/零 Turn。当前 Session 不可读时保留草稿,恢复后只向原会话发送一次;返回页面、只查状态和同 ID 不同 Session 均不重复。模拟浏览器 API 不证明模型意图质量;独立 HTTP/native store 验证生产读写边界,未跑发行限定的付费模型评测。
最终 premerge 和当前范围质量回执通过;无必需检查失败或 manual hold。公开安全扫描、diff hygiene 和签名通过,未查询或等待远端 CI。相邻重构抽取共用读恢复,保留既有执行 owner;没有增加旧数据平行解析分支。
我的整体评价
APPROVE:这是可独立交付的恢复体验,不是把“已转交”当完成。组合后的主干启动反馈与本 PR 读恢复均有打包证据,缺失历史不会触发工作,继续路径不要求用户重填。按明确授权合并当前提交;完整跨主机恢复和实际接收方采用仍不由这项单独改动证明。
No blocking finding. Residual risk: browser provider fixtures do not certify model intent quality; installed readback follows merge.
English verdict: APPROVE - exact head c27f7db9c46ee5d51d9ff5e21874f508effc2e86; integration blockers resolved and affected native/packaged invariants plus risk-based premerge pass. Owner-authorized merge; broader adoption and installed delivery are separate readbacks.
Goal And Delivered Outcome
A temporarily unreadable older Session currently hides the entire App conversation. For “接着昨天的做。” / “Pick up where we left off,” readable messages should remain visible and retain their original result locations.
This change isolates failed history reads in a shared TypeScript boundary used by both steward and Goal conversations. Only missing records retry, with backoff and a visible retry control. If the selected executor’s current Session is unreadable, Send and Enter wait for that record while preserving the draft; recovery continues once in the original Session. Another executor’s readable Session cannot bypass that gate.
main; owning contract: App conversation and async inbox RFC, roadmap R1 recovery.Validation
2a0f224259be1a160991d01adb4eccd4f0488a01. Functional build/browser/runtime checks ran at16d055fa67116f293ecb7746c9aa8eb9a950b521; the final commit only refreshes two source-line locators. Native premerge and census validation ran on final head.npm run build: strict TS, desktop and bundled Chat asset integrity; configured Python mypy and changed-file Ruff; public/private scan and DCO.npm run test:conversation-returns; focused chat CORS, input-validation and active-turn suites (105 passed). Exact session/message IDs and user/answer roles retain distinct records.npm run smoke:conversation-historyagainst disposable real Chat HTTP/file storage: partial read, channel isolation, missing-only retry, unchanged storage and zero Turns.uv run --extra test python examples/loopx-chat-server-smoke.py: projection exchange IDs equal persisted user/answer records.npm run smoke:personal-workspace-packaged: all 25 browser scenarios. Current-session failure blocks Send and Enter, preserves the draft, resumes the exact Session once; streamed and projection-only navigation do not duplicate messages.649826221289cd4cb3dd8880d016e0afbbaca0fc, then pass on head. The injected fault changes reads only. Healthy recovery and existing packaged journeys remain covered.loopx canary premerge --from-git-diff --goal-id GOAL: 16 selected and 5 direct checks; exact-scope quality receipt verified. The first run exposed stale census line locators, corrected with the existing generator and revalidated.Coverage: source packaging and isolated real Chat storage are verified. Live providers, full native delegation/adoption and an installed desktop rollout were not exercised. No model API calls, historical request replays or active Goal state mutations were used for validation.
Frontend / Visual Evidence
Type of Change
LoopX Area
Shared-authority RFC fixture impact
N/A: no authority-store, provider-routing, promotion or compatibility-projection change. This is shared App read reconciliation using the existing Chat store and HTTP contract; the projection response adds stored message IDs without new effects.
Boundary Checklist